// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.uast

import com.intellij.lang.Language
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.psi.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.uast.analysis.UastAnalysisPlugin

interface UastLanguagePlugin {
  companion object {
    val extensionPointName = ExtensionPointName<UastLanguagePlugin>("org.jetbrains.uast.uastLanguagePlugin")
    private val extensions by lazy(LazyThreadSafetyMode.PUBLICATION) { extensionPointName.extensionList }

    fun getInstances(): Collection<UastLanguagePlugin> = extensions

    fun byLanguage(language: Language): UastLanguagePlugin? = extensions.firstOrNull { it.language === language }
  }

  data class ResolvedMethod(val call: UCallExpression, val method: PsiMethod)
  data class ResolvedConstructor(val call: UCallExpression, val constructor: PsiMethod, val clazz: PsiClass)

  val language: Language

  /**
   * Checks if the file with the given [fileName] is supported.
   *
   * @param fileName the source file name.
   * @return true, if the file is supported by this converter, false otherwise.
   */
  fun isFileSupported(fileName: String): Boolean

  /**
   * Returns the converter priority. Might be positive, negative or 0 (Java's is 0).
   * UastConverter with the higher priority will be queried earlier.
   *
   * Priority is useful when a language N wraps its own elements (NElement) to, for example, Java's PsiElements,
   *  and Java resolves the reference to such wrapped PsiElements, not the original NElement.
   * In this case N implementation can handle such wrappers in UastConverter earlier than Java's converter,
   *  so N language converter will have a higher priority.
   */
  val priority: Int

  /**
   * Converts a PSI element, the parent of which already has an UAST representation, to UAST.
   *
   * @param element the element to convert
   * @param parent the parent as an UAST element, or null if the element is a file
   * @param requiredType the expected type of the result.
   * @return the converted element, or null if the element isn't supported or doesn't match the required result type.
   */
  fun convertElement(element: PsiElement, parent: UElement?, requiredType: Class<out UElement>? = null): UElement?

  /**
   * Converts a PSI element, along with its chain of parents, to UAST.
   *
   * @param element the element to convert
   * @param requiredType the expected type of the result.
   * @return the converted element, or null if the element isn't supported or doesn't match the required result type.
   */
  fun convertElementWithParent(element: PsiElement, requiredType: Class<out UElement>?): UElement?

  fun getMethodCallExpression(
    element: PsiElement,
    containingClassFqName: String?,
    methodName: String
  ): ResolvedMethod?

  fun getConstructorCallExpression(
    element: PsiElement,
    fqName: String
  ): ResolvedConstructor?

  fun getMethodBody(element: PsiMethod): UExpression? {
    if (element is UMethod) return element.uastBody
    return (convertElementWithParent(element, null) as? UMethod)?.uastBody
  }

  fun getInitializerBody(element: PsiClassInitializer): UExpression {
    if (element is UClassInitializer) return element.uastBody
    return (convertElementWithParent(element, null) as? UClassInitializer)?.uastBody ?: UastEmptyExpression(null)
  }

  fun getInitializerBody(element: PsiVariable): UExpression? {
    if (element is UVariable) return element.uastInitializer
    return (convertElementWithParent(element, null) as? UVariable)?.uastInitializer
  }

  /**
   * Returns true if the expression value is used.
   * Do not rely on this property too much, its value can be approximate in some cases.
   */
  fun isExpressionValueUsed(element: UExpression): Boolean

  @JvmDefault
  @Suppress("UNCHECKED_CAST")
  fun <T : UElement> convertElementWithParent(element: PsiElement, requiredTypes: Array<out Class<out T>>): T? =
    when {
      requiredTypes.isEmpty() -> convertElementWithParent(element, null)
      requiredTypes.size == 1 -> convertElementWithParent(element, requiredTypes.single())
      else -> convertElementWithParent(element, null)
        ?.takeIf { result -> requiredTypes.any { it.isAssignableFrom(result.javaClass) } }
    } as? T


  @JvmDefault
  fun <T : UElement> convertToAlternatives(element: PsiElement, requiredTypes: Array<out Class<out T>>): Sequence<T> =
    sequenceOf(convertElementWithParent(element, requiredTypes)).filterNotNull()

  @JvmDefault
  val analysisPlugin: UastAnalysisPlugin?
    @ApiStatus.Experimental
    get() = null
}

inline fun <reified T : UElement> UastLanguagePlugin.convertOpt(element: PsiElement?, parent: UElement?): T? {
  if (element == null) return null
  return convertElement(element, parent, T::class.java) as? T
}

@Deprecated("will throw exception if conversion fails", ReplaceWith("convertOpt"))
inline fun <reified T : UElement> UastLanguagePlugin.convert(element: PsiElement, parent: UElement?): T {
  return convertElement(element, parent, T::class.java) as T
}

inline fun <reified T : UElement> UastLanguagePlugin.convertWithParent(element: PsiElement?): T? {
  if (element == null) return null
  return convertElementWithParent(element, T::class.java) as? T
}